iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
Software Development

以MicroPython在ESP32上實作Insulin Delivery Service系列 第 8

Day 08 - 封裝 BLE 相關功能 (3)

  • 分享至 

  • xImage
  •  

昨天本喵遭受這突如其來的暴擊後,重新審視了一遍自己的程式碼,但並沒有發現與重構前有什麼重大不同的部分,除了一點外,就是在 BLE ISR 裡呼叫 micropython.schedule() 來處理 _IRQ_GATTS_READ_REQUEST!

4. 釐清問題

為了確認問題點,將讀取回應改為如下:

_counter = 0
_rsp = bytearray(1)

def _send_read_rsp(value_handle):
    global _counter

    _counter += 1
    _rsp[0] = _counter & 0xFF

    # 將要回覆的讀取要求寫入 characteristic 裡
    ble.stack.gatts_write(value_handle, _rsp)
    common.logger.write(f"Send read response: {_rsp}")

重新執行測試,發現原來 nRF Connect app 每次讀取到的值,都是更新 IDD Features 前的值:

讀取次序 IDD Features 當前內容 nRF Connect App 讀到的值
0 None N/A
1 1 None
2 2 1
3 3 2

這是怎麼回事呢?若去察看 _ble_isr() 對 _IRQ_GATTS_READ_REQUEST 的處理,很快就會了解發生了什麼事:

def _ble_isr(self, event, data):
    if event == _IRQ_GATTS_READ_REQUEST:
        conn_handle, value_handle = data
        micropython.schedule(_send_read_rsp, value_handle)

def _send_read_rsp(value_handle):
    # 將要回覆的讀取要求寫入 characteristic 裡
    ble.stack.gatts_write(value_handle, _rsp)

咱們的流程是:

  1. 執行 ISR,事件為 _IRQ_GATTS_READ_REQUEST 時,安排 _send_read_rsp() 在 ISR 結束後處理。
  2. ISR 結束。
  3. 執行 _send_read_rsp()。
  4. ble.stack.gatts_write() 更新 IDD Features。

問題點在於更新 IDD Features 是在步驟 4,而 MicroPython 是在步驟 2 結束前,就已讀取 IDD Features 的值,然後安排送出。

咱們可以佐以 MicroPython v1.25.0 的原始碼來驗證此猜想。

4-1. mp_bluetooth_gatts_on_read_request @ extmod/modbluetooth.c

mp_int_t mp_bluetooth_gatts_on_read_request(uint16_t conn_handle, uint16_t value_handle) {
    mp_int_t args[] = {conn_handle, value_handle};
    mp_obj_t result = invoke_irq_handler(
        MP_BLUETOOTH_IRQ_GATTS_READ_REQUEST,
        args,
        2,
        0,
        NULL_ADDR,
        NULL_UUID,
        NULL_DATA,
        NULL_DATA_LEN,
        0
    );
    // Return non-zero from IRQ handler to fail the read.
    mp_int_t ret = 0;
    mp_obj_get_int_maybe(result, &ret);
    return ret;
}

invoke_irq_handler() 的工作,就是將 BLE 中斷事件,傳給在 bluetooth.BLE.irq() 註冊的 ISR,等 ISR 處理完後,返回 ISR 的回傳值。

而哪個函數會呼叫 mp_bluetooth_gatts_on_read_request() 呢?就是 characteristic_access_cb()。

4-2. characteristic_access_cb @ extmod/nimble/modbluetooth_nimble.c

為了不干擾閱讀,本喵只列出相關部分。

static int characteristic_access_cb(uint16_t conn_handle, uint16_t value_handle, struct ble_gatt_access_ctxt *ctxt, void *arg) {
    mp_bluetooth_gatts_db_entry_t *entry;
    switch (ctxt->op) {
        case BLE_GATT_ACCESS_OP_READ_CHR:
        case BLE_GATT_ACCESS_OP_READ_DSC: {
            DEBUG_printf("write for %d %d (op=%d)\n", conn_handle, value_handle, ctxt->op);
            // Allow Python code to override (by using gatts_write), or deny (by returning false) the read.
            // Note this will be a no-op if the ringbuffer implementation is being used (i.e. the stack isn't
            // run in the scheduler). The ringbuffer is not used on STM32 and Unix-H4 only.
            int req = mp_bluetooth_gatts_on_read_request(conn_handle, value_handle);
            if (req) {
                return req;
            }

            entry = mp_bluetooth_gatts_db_lookup(MP_STATE_PORT(bluetooth_nimble_root_pointers)->gatts_db, value_handle);
            if (!entry) {
                return BLE_ATT_ERR_ATTR_NOT_FOUND;
            }

            if (os_mbuf_append(ctxt->om, entry->data, entry->data_len)) {
                return BLE_ATT_ERR_INSUFFICIENT_RES;
            }

            return 0;
        }
    }
    return BLE_ATT_ERR_UNLIKELY;
}

可以看到,若 mp_bluetooth_gatts_on_read_request() 回傳 0,那麼就會由 mp_bluetooth_gatts_db_lookup() 取得與 value_handle 相關聯的 characteristic 的值,然後用 os_mbuf_append() 送出。

5. 解決方法

由以上分析,可以知道若要回應讀取事件,必須在離開 ISR 前,便將資料以 bluetooth.BLE.gatts_write() 寫進 characteristic:

gatts_write(value_handle: int, data: bytes)

可是有幾個要求:

  1. ISR 內不能配置記憶體。
  2. 每次讀取回應的資料長度並非固定,亦即每次寫入所需的 bytes 長度不一。

而 gatts_write() 有個限制:

不支援寫入部分資料。若要傳送部分資料,須對資料進行切片,而這會造成記憶體分配。

對於要求 2,至少有 2 種方案:

  1. 每個 characteristic 本身就會維護自己的內容,讓其以 bytes 或 bytearray 保存,待須讀取時,直接取用即可。
    但此方法要求 characteristic 每次更新資料時,須要將其內容轉為位元組型式,這對 characteristic 內部運作並不方便。而且有時候,characteristic 的資料是由其他類別提供,會導致其他類別更新時,也要一併更新此 characteristic。
  2. 提供一個讀取回應所需的最大長度的 bytearray,當須回應時,將 characteristic 內容寫入其中。
    但這會觸碰到限制,而違反要求 1。

為了解決「gatts_write() 的限制」,但又不違反「ISR 內不能配置記憶體」,有個很簡單的方法:

使用 memoryview。

memoryview 就像對底層記憶體的一個觀看窗口,對它進行切片時,不會造成切片部分被複製到一新的記憶體區域。雖然還是需要創建 memoryview 物件的記憶體,但若連這都禁止,那本喵還真不知道要怎麼在 MicroPython 平台處理中斷了。

於是,咱們可以這樣更新 _ble_isr() 和 _send_read_rsp():

def _ble_isr(self, event, data):
    if event == _IRQ_GATTS_READ_REQUEST:
        conn_handle, value_handle = data

        _send_read_rsp(value_handle)

_counter = 0
_rsp = bytearray(20)
_rsp_mv = memoryview(_rsp)

def _send_read_rsp(value_handle):
    global _counter

    _counter += 1
    _rsp_mv[0] = _counter & 0xFF

    # 將要回覆的讀取要求寫入 characteristic 裡
    ble.stack.gatts_write(value_handle, _rsp_mv[:1])

總算結束 BLE 的封裝了
本來預計只用一天來寫,沒想卻寫了三天 ...
(。ŏ_ŏ)


上一篇
Day 07 - 封裝 BLE 相關功能 (2) 之翻車現場
下一篇
Day 09 - 實作最簡單的 Characteristic - IDD Features
系列文
以MicroPython在ESP32上實作Insulin Delivery Service31
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言